package edu.gmu.projects.fireresponder.relevancyengine;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import android.location.LocationListener;
import android.os.Bundle;
import android.util.Log;
import edu.gmu.projects.fireresponder.FirstResponder;
import edu.gmu.projects.fireresponder.comm.IFirstResponderAlertListener;
import edu.gmu.projects.fireresponder.environment.Asset;
import edu.gmu.projects.fireresponder.environment.Building;
import edu.gmu.projects.fireresponder.environment.Location;
import edu.gmu.projects.fireresponder.environment.Asset.AssetStatus;
import edu.gmu.projects.fireresponder.fusionengine.FusionEngine;
import edu.gmu.projects.fireresponder.fusionengine.FusionEngineListener;
import edu.gmu.projects.fireresponder.fusionengine.FusionEngine.ProbabilityCategory;
import edu.gmu.projects.fireresponder.missionplanner.MissionPlan;
import edu.gmu.projects.fireresponder.missionplanner.MissionPlanner;
import edu.gmu.projects.fireresponder.missionplanner.SituationEnvironment;
import edu.gmu.projects.fireresponder.xml.alerts.Alert;
import edu.gmu.projects.fireresponder.xml.alerts.BuildingClearedAlert;
import edu.gmu.projects.fireresponder.xml.alerts.BuildingDroppedAlert;
import edu.gmu.projects.fireresponder.xml.alerts.FirstResponderAlert;
import edu.gmu.projects.fireresponder.xml.alerts.ResponderLocationAlert;

public class RelevanceEngine implements FusionEngineListener, LocationListener,
		IFirstResponderAlertListener {
	public static enum RelevanceScale {
		NOT_RELEVANT, NORMAL_RELEVANCE, MODERATE_RELEVANCE, HIGH_RELEVANCE, VERY_HIGH_RELEVANCE
	};

	// FIRE_PEOPLE_WEIGHT + SCALED_TIME_WEIGHT + FUTURE_FIRE_WEIGHT must equal 1
	private static double FIRE_PEOPLE_WEIGHT = 0.5;
	private static double FUTURE_FIRE_WEIGHT = 0.3;
	private static double SCALED_TIME_WEIGHT = 0.2;
	private static double SCALED_TIME_POWER = 2.0;

	private List<IRelevanceEngineListener> listeners;
	private MissionPlanner planner;
	private FusionEngine fusionEngine;
	private Location currentLocation;
	private Asset currentMostRelevant;

	public RelevanceEngine(MissionPlanner planner) {
		this.planner = planner;
		listeners = new ArrayList<IRelevanceEngineListener>();
	}

	public void setFusionEngine(FusionEngine engine) {
		this.fusionEngine = engine;
		planner.setFusionEngine(engine);
	}

	public void loadPlanFromStream(InputStream stream) {
		planner.loadPlanFromStream(stream);

		Iterator<String> iter = planner.getMission().keySet().iterator();

		currentMostRelevant = getMostRelevantAsset();

		while (iter.hasNext()) {
			Asset asset = planner.getMission().get(iter.next());

			this.fireProbabilityUpdated(asset);
			this.presenceProbabilityUpdated(asset);
		}

		if (currentMostRelevant != null) {
			for (IRelevanceEngineListener l : listeners) {
				l.onCurrentMissionTaskUpdated(currentMostRelevant);
			}
		}
	}

	public Map<String, Asset> getMissionPlan() {
		return planner.getMission();
	}

	public void clearMissionPlan() {
		currentMostRelevant = null;
		planner.clearMissionPlan();
	}

	public void clearEnvironmentInfo() {
		planner.clearEnvironmentInfo();
	}

	public void clearAll() {
		planner.clearAll();
	}

	public Asset getEnvironmentAssetById(String id) {
		return planner.getEnvironmentAssetMap().get(id);
	}

	public boolean isAssetInMission(Asset asset) {
		return planner.getMissionAssets().contains(asset);
	}

	public void onResponderSetFireSeen(String assetId) {
		Asset asset = planner.getMission().get(assetId);

		if (asset != null) {
			asset.setProbabilityOfFire(1.0);
			fireProbabilityUpdated(asset);
		}
	}

	public void onResponderSetPeopleSeen(String assetId) {
		Asset asset = planner.getMission().get(assetId);

		if (asset != null) {
			asset.setProbabilityOfPresence(1.0);
			presenceProbabilityUpdated(asset);
		}
	}

	public void onResponderSetCompleted(String assetId) {
		Asset asset = planner.getMission().get(assetId);

		onResponderSetCompleted(asset);
	}

	public void onResponderSetCompleted(Asset asset) {
		if (asset != null) {
			asset.setStatus(AssetStatus.COMPLETE);

			if (asset instanceof Building) {
				for (IRelevanceEngineListener l : listeners) {
					l.onBuildingCleared((Building) asset);
				}
			}

			onPotentialMostRelevantChanged(asset);
		}
	}

	public void onResponderSetUnableToReach(String assetId) {
		Asset asset = planner.getMission().get(assetId);

		// If asset is marked complete, asset cannot be marked as unable
		if (asset != null
				&& asset.getStatus().compareTo(AssetStatus.COMPLETE) != 0) {

			asset.setStatus(AssetStatus.FR_UNABLE);

			if (asset instanceof Building) {
				for (IRelevanceEngineListener l : listeners) {
					l.onResponderUnable((Building) asset);
				}
			}

			onPotentialMostRelevantChanged(asset);
		}
	}

	private void onPotentialMostRelevantChanged(Asset asset) {
		if (currentMostRelevant != null && currentMostRelevant.equals(asset)) {
			currentMostRelevant = getMostRelevantAsset();

			if (currentMostRelevant != null) {
				for (IRelevanceEngineListener l : listeners) {
					l.onCurrentMissionTaskUpdated(currentMostRelevant);
				}
			}
		}
	}

	private Asset getMostRelevantAsset() {
		Asset current = null;

		Iterator<String> iter = planner.getMission().keySet().iterator();

		// Prime the pump - finds first "active" building
		while (iter.hasNext()) {
			Asset a = planner.getMission().get(iter.next());

			if ((a instanceof Building)
					&& (a.getStatus() == AssetStatus.NOT_IN_PROGRESS || a
							.getStatus() == AssetStatus.IN_PROGRESS)) {
				current = a;
				break;
			}
		}

		// Find building with highest relevance value
		while (iter.hasNext()) {
			Asset a = planner.getMission().get(iter.next());

			if ((a instanceof Building)
					&& (a.getStatus() == AssetStatus.NOT_IN_PROGRESS || a
							.getStatus() == AssetStatus.IN_PROGRESS)
					&& a.getRelevanceValue() > current.getRelevanceValue()) {
				current = a;
			}
		}

		return current;
	}

	public Asset getClosestAsset(Location locationIn) {
		return planner.getClosestAsset(locationIn);
	}

	public void fireProbabilityUpdated(Asset asset) {
		if (isAssetInMission(asset)) {
			// Always update relevance value
			onAssetValuesUpdated(asset);
			fireProbabilityUpdatedGui(asset);
		}
	}

	private void fireProbabilityUpdatedGui(Asset asset) {
		ProbabilityCategory category = FusionEngine
				.getProbabilityCategory(asset.getProbabilityOfFire());

		Log.i("RelevanceEngine", String.format(
				"probability of fire at %s is %s", asset.getId(), category));

		// Notify at these thresholds
		if (category.compareTo(ProbabilityCategory.IMPROBABLE) >= 0) {
			for (IRelevanceEngineListener l : listeners) {
				l.onFireProbabilityUpdated(asset);
			}
		}
	}

	public void presenceProbabilityUpdated(Asset asset) {
		if (isAssetInMission(asset)) {
			// Always update relevance value
			onAssetValuesUpdated(asset);
			updatePresenceGui(asset);
		}
	}

	private void updatePresenceGui(Asset asset) {
		// Only concerned about people in buildings
		if (asset instanceof Building) {
			// Stop notifying about people if asset is complete
			if (asset.getStatus() != AssetStatus.COMPLETE) {

				ProbabilityCategory category = FusionEngine
						.getProbabilityCategory(asset
								.getProbabilityOfPresence());

				// Notify when probability sufficient and insufficient
				if (category.compareTo(ProbabilityCategory.POSSIBLE) >= 0) {
					for (IRelevanceEngineListener l : listeners) {
						l.onPeopleInBuildingUpdate((Building) asset);
					}
					// TODO May need to tweak
				} else if (category.compareTo(ProbabilityCategory.IMPROBABLE) <= 0) {
					for (IRelevanceEngineListener l : listeners) {
						l.onNoPeopleInBuildingUpdate((Building) asset);
					}
				}
			}
		}
	}

	private void onAssetValuesUpdated(Asset asset) {
		Log.i("RelevanceEngine", "Asset values have been updated");

		double probOfFire = asset.getProbabilityOfFire();
		double probOfPeople = asset.getProbabilityOfPresence();

		int estimatedTimeTo = estimatedTimeToAsset(asset);
		double probOfFireThen = fusionEngine.getFireProbability(asset,
				estimatedTimeTo * 1000);

		double percentCovered = fusionEngine.getPercentageOnFire(0.01, asset);

		double relevance = calculateRelevanceValue(probOfFire, probOfPeople,
				estimatedTimeTo, probOfFireThen);
		asset.setRelevanceValue(relevance);

		if (asset.getStatus().compareTo(AssetStatus.BLOCKED) == 0
				|| percentCovered >= 0.9
				|| estimatedTimeTo == Integer.MAX_VALUE) {

			// TODO Re-plan mission and notify responder
			Log.i("RelevanceEngine", String.format("%s is not reachable, "
					+ "re-plan mission and notify responder", asset.getId()));

			for (IRelevanceEngineListener l : listeners) {
				l.onLoadingNewMisisonPlan();
			}

			Map<String, Asset> oldMission = planner.getMission();
			Map<String, Asset> newMission = planner.updateMission();

			if (MissionPlan.planChanged(oldMission, newMission)) {

				String jsonMission = MissionPlan.buildString(newMission);

				for (IRelevanceEngineListener l : listeners) {
					l.onLoadNewMissionPlan(jsonMission);
				}

				Iterator<String> iter = newMission.keySet().iterator();
				currentMostRelevant = getMostRelevantAsset();

				while (iter.hasNext()) {
					Asset a = newMission.get(iter.next());

					fireProbabilityUpdatedGui(a);
					updatePresenceGui(a);
				}

				for (IRelevanceEngineListener l : listeners) {
					l.onLoadedNewMissionPlan();
				}

				if (currentMostRelevant != null) {
					for (IRelevanceEngineListener l : listeners) {
						l.onCurrentMissionTaskUpdated(currentMostRelevant);
					}
				}
			} else {
				Log.i("RelevanceEngine",
						"Mission planner determined i should not change");
			}
		} else {
			Asset mostRelevant = getMostRelevantAsset();

			if (mostRelevant != null && currentMostRelevant != mostRelevant) {
				currentMostRelevant = mostRelevant;

				for (IRelevanceEngineListener l : listeners) {
					l.onCurrentMissionTaskUpdated(currentMostRelevant);
				}
			}
		}
	}

	/*
	 * An assumption made here that responder is on plan so estimated time to
	 * asset is a good indication of how far away the responder is to the asset.
	 * This way I don't need to know where the responder is and calculate
	 * distance from asset to responder.
	 */
	private double calculateRelevanceValue(double probOfFire,
			double probOfPeople, int estimatedTimeTo, double probOfFireThen) {

		double estimatedTimeToNorm = estimatedTimeTo > 3600 ? 1.0
				: estimatedTimeTo / 3600.0;
		double scaledTimeTerm = 1.0 - Math.pow(estimatedTimeToNorm, SCALED_TIME_POWER);

		return FIRE_PEOPLE_WEIGHT * (probOfPeople * probOfFire)
				+ SCALED_TIME_WEIGHT * scaledTimeTerm + FUTURE_FIRE_WEIGHT
				* probOfFireThen;
	}

	public int estimatedTimeToAsset(Asset asset) {
		int estimatedTime = 0;

		Iterator<String> iter = planner.getMission().keySet().iterator();

		while (iter.hasNext()) {
			String key = iter.next();

			Asset a = planner.getMission().get(key);

			switch (a.getStatus()) {
			case BLOCKED: // Unreachable
				return Integer.MAX_VALUE;
			case IN_PROGRESS: // Reachable
			case NOT_IN_PROGRESS:
				estimatedTime += a.getEstimatedTimeToComplete();
			case COMPLETE: // Don't care anymore
			case FR_UNABLE:
				break;
			}

			if (a == asset) {
				break;
			}
		}

		return estimatedTime;
	}

	public void receivedResponderLocationAlert(ResponderLocationAlert r) {
		SituationEnvironment env = SituationEnvironment
				.getSituationEnvironment();

		// If its not my own beacon message
		if (r.getSender().compareTo(env.getSelf().getId()) != 0) {
			FirstResponder fr = env.findTeamMember(r.getSender());

			if (fr != null) {
				fr.setLocation(r.getLocation());
			} else {
				fr = env.addTeamMember(r.getSender(), r.getLocation());
			}

			for (IRelevanceEngineListener l : listeners) {
				l.onResponderLocationUpdated(fr);
			}
		}
	}

	public void receivedFirstResponderAlert(FirstResponderAlert alert) {
		// Send notification when a message or an image is attached to the alert
		if ((alert.getMessage() != null && alert.getMessage().length() != 0)
				|| (alert.getImageContents() != null && alert
						.getImageContents().length != 0)) {
			for (IRelevanceEngineListener l : listeners) {
				l.onMessageOrImageReceived(alert);
			}
		}

		for (Alert embedded : alert.getEmbeddedAlerts()) {
			Log.i("RelevanceEngine", "Alert received from " + embedded.getId());

			SituationEnvironment env = SituationEnvironment
					.getSituationEnvironment();

			if (embedded instanceof BuildingClearedAlert) {
				Building asset = (Building) env.getClosestAsset(embedded
						.getLocation(), Building.class);
				if (asset != null) {
					asset.setStatus(AssetStatus.COMPLETE);

					for (IRelevanceEngineListener l : listeners) {
						l.onBuildingCleared(asset);
					}

					onPotentialMostRelevantChanged(asset);
				}
			} else if (embedded instanceof BuildingDroppedAlert) {
				Building asset = (Building) env.getClosestAsset(embedded
						.getLocation(), Building.class);

				FirstResponder self = SituationEnvironment
						.getSituationEnvironment().getSelf();

				if (((BuildingDroppedAlert) embedded).getSender().compareTo(
						self.getId()) == 0) {
					if (asset.getStatus().compareTo(AssetStatus.COMPLETE) != 0) {
						asset.setStatus(AssetStatus.FR_UNABLE);

						for (IRelevanceEngineListener l : listeners) {
							l.onResponderUnable(asset);
						}
					}
				} else {
					if (asset.getStatus().compareTo(AssetStatus.COMPLETE) != 0) {
						asset.setStatus(AssetStatus.TEAM_MEMBER_UNABLE);

						for (IRelevanceEngineListener l : listeners) {
							l.onTeamMemberUnable(asset);
						}
					}
				}

				onPotentialMostRelevantChanged(asset);
			}
		}
	}

	public static RelevanceScale getRelevanceScale(double relevance) {
		if (0 < relevance && relevance < 0.2) {
			return RelevanceScale.NORMAL_RELEVANCE;
		} else if (0.4 <= relevance && relevance < 0.6) {
			return RelevanceScale.MODERATE_RELEVANCE;
		} else if (0.6 <= relevance && relevance < 0.8) {
			return RelevanceScale.HIGH_RELEVANCE;
		} else if (0.8 <= relevance && relevance <= 1) {
			return RelevanceScale.VERY_HIGH_RELEVANCE;
		}

		return RelevanceScale.NOT_RELEVANT;
	}

	public static String getRelevanceScaleText(RelevanceScale scale) {
		switch (scale) {
		case NOT_RELEVANT:
			return "NONE";
		case NORMAL_RELEVANCE:
			return "NORMAL";
		case MODERATE_RELEVANCE:
			return "MODERATE";
		case HIGH_RELEVANCE:
			return "HIGH";
		case VERY_HIGH_RELEVANCE:
			return "VERY HIGH";
		}

		return "UNKNOWN";
	}

	public static String getRelevanceScaleText(double value) {
		return getRelevanceScaleText(getRelevanceScale(value));
	}

	public boolean addListener(IRelevanceEngineListener listener) {
		return listeners.add(listener);
	}

	public boolean removeListener(IRelevanceEngineListener listener) {
		return listeners.remove(listener);
	}

	public void onLocationChanged(android.location.Location location) {
		this.currentLocation = new Location(location.getLatitude(), location
				.getLongitude());

		Log.i("RelevanceEngine", "onLocationChanged: "
				+ currentLocation.toString());
	}

	public void onProviderDisabled(String provider) {
	}

	public void onProviderEnabled(String provider) {
	}

	public void onStatusChanged(String provider, int status, Bundle extras) {
	}
}